vimfandomcom-20200223-history
Act on text objects with custom functions
One of the nice features of Vim is the ability to operate on text objects, for example di) will delete text inside parentheses and dd will delete a line (see ). This tip provides a function to allow you to create your own such actions. For example, I like to use \u to open a URL. Using the mechanism below, \ui] will open a URL contained within square brackets, \u$ from the cursor to the end of the line, \uu an entire line, and v...\u the selected region in visual mode. With the helper functions below, setting this up is simple enough to be used for spur-of-the-moment needs such as reversing a string, computing MD5 sums, converting unicode characters to LaTeX, and so on. The script Put the following into your .vimrc file. In the next section I'll give some usage examples, then I'll explain how it works. " Adapted from unimpaired.vim by Tim Pope. function! s:DoAction(algorithm,type) " backup settings that we will change let sel_save = &selection let cb_save = &clipboard " make selection and clipboard work the way we need set selection=inclusive clipboard-=unnamed clipboard-=unnamedplus " backup the unnamed register, which we will be yanking into let reg_save = @@ " yank the relevant text, and also set the visual selection (which will be reused if the text " needs to be replaced) if a:type =~ '^\d\+$' " if type is a number, then select that many lines silent exe 'normal! V'.a:type.'$y' elseif a:type =~ '^.$' " if type is 'v', 'V', or '' (i.e. 0x16) then reselect the visual region silent exe "normal! `<" . a:type . "`>y" elseif a:type 'line' " line-based text motion silent exe "normal! 'V'y" elseif a:type 'block' " block-based text motion silent exe "normal! `\`y" else " char-based text motion silent exe "normal! `v`y" endif " call the user-defined function, passing it the contents of the unnamed register let repl = s:{a:algorithm}(@@) " if the function returned a value, then replace the text if type(repl) 1 " put the replacement text into the unnamed register, and also set it to be a " characterwise, linewise, or blockwise selection, based upon the selection type of the " yank we did above call setreg('@', repl, getregtype('@')) " relect the visual region and paste normal! gvp endif " restore saved settings and register value let @@ = reg_save let &selection = sel_save let &clipboard = cb_save endfunction function! s:ActionOpfunc(type) return s:DoAction(s:encode_algorithm, a:type) endfunction function! s:ActionSetup(algorithm) let s:encode_algorithm = a:algorithm let &opfunc = matchstr(expand(''), '\d\+_').'ActionOpfunc' endfunction function! MapAction(algorithm, key) exe 'nnoremap actions' .a:algorithm.' :call ActionSetup("'.a:algorithm.'")g@' exe 'xnoremap actions' .a:algorithm.' :call DoAction("'.a:algorithm.'",visualmode())' exe 'nnoremap actionsLine'.a:algorithm.' :call DoAction("'.a:algorithm.'",v:count1)' exe 'nmap '.a:key.' actions'.a:algorithm exe 'xmap '.a:key.' actions'.a:algorithm exe 'nmap '.a:key.a:keystrlen(a:key)-1.' actionsLine'.a:algorithm endfunction Usage examples An action is setup by calling MapAction. The first argument should be the name of a function (which you have created) and the second argument should be the key to bind to. If the function returns a value, the text is replaced, otherwise the text is unchanged. For example, the following causes r to reverse a string. The ReverseString function simply takes the string as an argument and reverses it. function! s:ReverseString(str) let out = join(reverse(split(a:str, '\zs')), '') " Remove a trailing newline that reverse() moved to the front. let out = substitute(out, '^\n', '', '') return out endfunction call MapAction('ReverseString', 'r') Now, typing ri) will reverse a string within parentheses, rr will reverse a line, and so on. It also works on visual selections. The following allows opening URLs with ui], uu, etc. Since the function doesn't return a value, the text is unchanged. function! s:OpenUrl(str) silent execute "!firefox ".shellescape(a:str, 1) redraw! endfunction call MapAction('OpenUrl','u') This one pipes the text through an external program. The selected text is piped into the given program and is replaced by that program's output. function! s:ComputeMD5(str) let out = system('md5sum |cut -b 1-32', a:str) " Remove trailing newline. let out = substitute(out, '\n$', '', '') return out endfunction call MapAction('ComputeMD5','M') How it works Consider the ReverseString example above. When MapAction is called, the first three lines create internal mappings named actionsReverseString and actionsLineReverseString. The next three lines of MapAction actually map these to your specified key sequence. The first nmap handles text objects such as ri). As you begin to type the sequence, r, the mapping calls ActionSetup("ReverseString") which in turn tells Vim that the ActionOpfunc function should handle the subsequent motion command. The s:encode_algorithm variable is what we use to remember what should be done once the motion command is received. It is set to ReverseString. Finally, the key sequence g@ is sent, causing Vim to listen for a motion command (e.g. i)). When you finish typing the motion command, ActionOpfunc gets called, which defers to DoAction('ReverseString', ...). For more information on this sequence of events, see . The xmap in MapAction is for visual mode. The second nmap is for line mode, for example rr or 3rr. In all cases, ultimately it is DoAction that gets called. The algorithm argument tells the user-defined function that should be called, and type gives the type of the text object. We first set some configuration options (which are saved and restored later). The next task is to read the selected text into a variable. This is done by first yanking into the unnamed register (a backup copy of this register is saved in reg_save and restored in the end). How the yank is done depends on the value of type, however we always begin by making a visual selection since this can be reused later to replace the text if needed. After the code is yanked into the unnamed register, we call the user-defined function (ReverseString in this example), passing it the contents of the unnamed register, into which the text was yanked. If the function returned a value, then the selection is reselected with gv and the new text pasted. Finally, the backed-up settings and the original content of the unnamed register are restored. Related plugins * handles all the creation of mappings (or custom commands); you just supply the transformation algorithm in the form of a Vim function! It is basically a generic utility plugin derived from the (static) unimpaired.vim plugin *Toop plugin allows you to easily map behaviour to text objects, also fixing some issues with new lines results that this snippet don't cover. Comments